using System;
using System.Collections.Generic;
using System.Globalization;
using System.Net;
using System.Net.Sockets;
using System.ServiceModel;
using System.ServiceModel.Channels;

namespace Microsoft.ServiceModel.Samples.UdpTransport
{
    internal class UdpDuplexChannel : ChannelBase, IDuplexChannel
    {
        private readonly MessageEncoder encoder;
        private readonly InputQueue<Message> messageQueue;
        private readonly UdpChannelFactory2 parent;
        private readonly EndpointAddress remoteAddress;
        private readonly IPEndPoint remoteEndPoint;
        private readonly Socket socket;
        private readonly Uri via;

        internal UdpDuplexChannel(UdpChannelFactory2 factory, EndpointAddress remoteAddress, Uri via,
                                  MessageEncoder encoder)
            : base(factory)
        {
            messageQueue = new InputQueue<Message>();

            // validate addressing arguments
            if (!string.Equals(via.Scheme, UdpConstants.Scheme, StringComparison.InvariantCultureIgnoreCase))
            {
                throw new ArgumentException(string.Format(CultureInfo.CurrentCulture,
                                                          "The scheme {0} specified in address is not supported.",
                                                          via.Scheme), "via");
            }

            if (via.IsDefaultPort)
            {
                throw new ArgumentException(string.Format(CultureInfo.CurrentCulture,
                                                          "The address {0} must specify a remote port.", via), "via");
            }

            // convert the Uri host into an IP Address
            IPAddress remoteIP;
            switch (via.HostNameType)
            {
                default:
                    throw new ArgumentException(string.Format(CultureInfo.CurrentCulture,
                                                              "Cannot determine the remote host address from {0}.",
                                                              this.via), "via");

                case UriHostNameType.IPv4:
                case UriHostNameType.IPv6:
                    remoteIP = IPAddress.Parse(via.Host);
                    break;

                case UriHostNameType.Basic:
                case UriHostNameType.Dns:
                    {
                        IPHostEntry hostEntry = Dns.GetHostEntry(via.Host);
                        if (hostEntry.AddressList.Length > 0)
                        {
                            remoteIP = hostEntry.AddressList[0];
                        }
                        else
                        {
                            throw new ArgumentException(string.Format(CultureInfo.CurrentCulture,
                                                                      "Failed to resolve remote host: {0}.", via.Host),
                                                        "via");
                        }
                        break;
                    }
            }

            if (remoteIP == null)
                throw new SystemException("Unable to determine remote IP address");

            if (factory.Multicast && !UdpChannelHelpers.IsInMulticastRange(remoteIP))
            {
                throw new ArgumentOutOfRangeException("remoteEndPoint", "Via must be in the valid multicast range.");
            }

            parent = factory;
            this.remoteAddress = remoteAddress;
            this.via = via;
            this.encoder = encoder;
            remoteEndPoint = new IPEndPoint(remoteIP, via.Port);
            socket = new Socket(remoteEndPoint.AddressFamily, SocketType.Dgram, ProtocolType.Udp);

            if (parent.Multicast)
            {
                socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, 1);
#if LATER // Support outgoing interface
                if (this.remoteEndPoint.AddressFamily == AddressFamily.InterNetwork)
                {
                    this.socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastInterface, IPAddress.HostToNetworkOrder((int)interfaceIndex));
                }
                else //  we are IPv6
                {
                    this.sendSocketV6.SetSocketOption(SocketOptionLevel.IPv6, SocketOptionName.MulticastInterface, (int)interfaceIndex);
                }
#endif
            }
        }

        public UdpDuplexChannel(UdpDuplexChannelListener listener2, List<Socket> sockets) : base(listener2)
        {
            encoder = listener2.MessageEncoderFactory.Encoder;
            messageQueue = new InputQueue<Message>();
            Sockets = sockets;
        }

        protected List<Socket> Sockets { get; set;}

        protected Socket GetSendSocket( AddressFamily addressFamily, Uri destination, out Exception exceptionToBeThrown)
        {
            Socket socket = null;
            exceptionToBeThrown = null;
      
            lock (base.ThisLock)
            {
                if (base.State == CommunicationState.Opened)
                {
                    for (int i = 0; i < this.Sockets.Count; i++)
                    {
                        if (addressFamily == this.Sockets[i].AddressFamily)
                        {
                            socket = this.Sockets[i];
                            break;
                        }
                    }
                    if (socket == null)
                    {
                        exceptionToBeThrown = new InvalidOperationException("RemoteAddressUnreachableDueToIPVersionMismatch" + destination);
                    }
                    return socket;
                }
                exceptionToBeThrown = CreateObjectDisposedException();
            }
            return socket;
        }

        protected static ObjectDisposedException CreateObjectDisposedException()
        {
            return new ObjectDisposedException(null, "Object disposed");
        }

 

 

 

 


        #region Input

        public EndpointAddress LocalAddress
        {
            get { return null; }
        }

        //Hands the message off to other components higher up the
        //channel stack that have previously called BeginReceive() 
        //and are waiting for messages to arrive on this channel.

        public override T GetProperty<T>()
        {
            if (typeof (T) == typeof (IInputChannel))
            {
                return (T) (object) this;
            }

            var messageEncoderProperty = encoder.GetProperty<T>();
            if (messageEncoderProperty != null)
            {
                return messageEncoderProperty;
            }

            return base.GetProperty<T>();
        }

        //Closes the channel ungracefully during error conditions.

        public Message Receive()
        {
            return Receive(DefaultReceiveTimeout);
        }

        public Message Receive(TimeSpan timeout)
        {
            Message message;
            if (TryReceive(timeout, out message))
            {
                return message;
            }
            throw CreateReceiveTimedOutException(this, timeout);
        }

        public IAsyncResult BeginReceive(AsyncCallback callback, object state)
        {
            return BeginReceive(DefaultReceiveTimeout, callback, state);
        }

        public IAsyncResult BeginReceive(TimeSpan timeout, AsyncCallback callback, object state)
        {
            return BeginTryReceive(timeout, callback, state);
        }

        public Message EndReceive(IAsyncResult result)
        {
            return messageQueue.EndDequeue(result);
        }

        public bool TryReceive(TimeSpan timeout, out Message message)
        {
            UdpChannelHelpers.ValidateTimeout(timeout);
            return messageQueue.Dequeue(timeout, out message);
        }

        public IAsyncResult BeginTryReceive(TimeSpan timeout, AsyncCallback callback, object state)
        {
            UdpChannelHelpers.ValidateTimeout(timeout);
            return messageQueue.BeginDequeue(timeout, callback, state);
        }

        public bool EndTryReceive(IAsyncResult result, out Message message)
        {
            return messageQueue.EndDequeue(result, out message);
        }

        public bool WaitForMessage(TimeSpan timeout)
        {
            UdpChannelHelpers.ValidateTimeout(timeout);
            return messageQueue.WaitForItem(timeout);
        }

        public IAsyncResult BeginWaitForMessage(TimeSpan timeout, AsyncCallback callback, object state)
        {
            UdpChannelHelpers.ValidateTimeout(timeout);
            return messageQueue.BeginWaitForItem(timeout, callback, state);
        }

        public bool EndWaitForMessage(IAsyncResult result)
        {
            return messageQueue.EndWaitForItem(result);
        }

        internal void Dispatch(Message message)
        {
            messageQueue.EnqueueAndDispatch(message);
        }

        protected override void OnAbort()
        {
            messageQueue.Close();
        }

        //Closes the channel gracefully during normal conditions.
        protected override void OnClose(TimeSpan timeout)
        {
            messageQueue.Close();
        }

        protected override IAsyncResult OnBeginClose(TimeSpan timeout, AsyncCallback callback, object state)
        {
            OnClose(timeout);
            return new CompletedAsyncResult(callback, state);
        }

        protected override void OnEndClose(IAsyncResult result)
        {
            CompletedAsyncResult.End(result);
        }

        protected override void OnOpen(TimeSpan timeout)
        {
        }

        protected override IAsyncResult OnBeginOpen(TimeSpan timeout, AsyncCallback callback, object state)
        {
            return new CompletedAsyncResult(callback, state);
        }

        protected override void OnEndOpen(IAsyncResult result)
        {
            CompletedAsyncResult.End(result);
        }

        private static TimeoutException CreateReceiveTimedOutException(IInputChannel channel, TimeSpan timeout)
        {
            if (channel.LocalAddress != null)
            {
                return new TimeoutException(
                    string.Format(
                        "Receive on local address {0} timed out after {1}. The time allotted to this operation may have been a portion of a longer timeout.",
                        channel.LocalAddress.Uri.AbsoluteUri, timeout));
            }
            
            return new TimeoutException(
                string.Format(
                    "Receive timed out after {0}. The time allotted to this operation may have been a portion of a longer timeout.",
                    timeout));
        }

        #endregion


        #region IDuplexChannel Members

        public void Send(Message message)
        {
            base.ThrowIfDisposedOrNotOpen();

            Exception ex;
            var remoteEP = GetEndpointFromMessage(message);
            var socket = GetSendSocket(remoteEP.AddressFamily, message.Headers.To, out ex); // new Socket(remoteEP.AddressFamily, SocketType.Dgram, ProtocolType.Udp);
            ArraySegment<byte> messageBuffer = EncodeMessage(message);

            try
            {
                int bytesSent = socket.SendTo(messageBuffer.Array, messageBuffer.Offset, messageBuffer.Count,
                                              SocketFlags.None, remoteEP);

                if (bytesSent != messageBuffer.Count)
                {
                    throw new CommunicationException(string.Format(CultureInfo.CurrentCulture,
                                                                   "A Udp error occurred sending a message to {0}.",
                                                                   remoteEP));
                }
            }
            catch (SocketException socketException)
            {
                throw UdpChannelHelpers.ConvertTransferException(socketException);
            }
            finally
            {
                // we need to make sure buffers are always returned to the BufferManager
               // parent.BufferManager.ReturnBuffer(messageBuffer.Array);
            }
        }

        private static EndPoint GetEndpointFromMessage(Message message)
        {
            var via = message.Headers.To;
            return new IPEndPoint(IPAddress.Parse(via.DnsSafeHost), via.Port);
        }

        public void Send(Message message, TimeSpan timeout)
        {
            // UDP does not block so we do not need timeouts.
            Send(message);
        }

        #endregion

        #region Send_Asynchronous

        public IAsyncResult BeginSend(Message message, AsyncCallback callback, object state)
        {
            base.ThrowIfDisposedOrNotOpen();
            var remoteEP = GetEndpointFromMessage(message);
            Exception ex;
            var socket = GetSendSocket(remoteEP.AddressFamily, message.Headers.To, out ex);

            return new SendAsyncResult(this, socket, remoteEP, message, callback, state);
        }

        public IAsyncResult BeginSend(Message message, TimeSpan timeout, AsyncCallback callback, object state)
        {
            // UDP does not block so we do not need timeouts.
            return BeginSend(message, callback, state);
        }

        public void EndSend(IAsyncResult result)
        {
            SendAsyncResult.End(result);
        }

        public EndpointAddress RemoteAddress
        {
            get { return remoteAddress; }
        }

        public Uri Via
        {
            get { return via; }
        }

        /// <summary>
        /// Implementation of async send for Udp. 
        /// </summary>
        private class SendAsyncResult : AsyncResult
        {
            private readonly UdpDuplexChannel channel;
            private ArraySegment<byte> messageBuffer;
            private readonly Socket socket;
            private readonly EndPoint remoteEndpoint;

            public SendAsyncResult(UdpDuplexChannel channel, Socket socket, EndPoint remoteEndPoint, Message message, AsyncCallback callback, object state)
                : base(callback, state)
            {
                this.channel = channel;
                this.socket = socket;
                this.remoteEndpoint = remoteEndPoint;
                messageBuffer = channel.EncodeMessage(message);
                try
                {
                    IAsyncResult result;
                    try
                    {
                        result = socket.BeginSendTo(messageBuffer.Array, messageBuffer.Offset,
                                                            messageBuffer.Count,
                                                            SocketFlags.None, remoteEndpoint, OnSend, this);
                    }
                    catch (SocketException socketException)
                    {
                        throw UdpChannelHelpers.ConvertTransferException(socketException);
                    }

                    if (!result.CompletedSynchronously)
                        return;

                    CompleteSend(result, true);
                }
                catch
                {
                    CleanupBuffer();
                    throw;
                }
            }

            private void CleanupBuffer()
            {
                if (messageBuffer.Array != null)
                {
                    channel.parent.BufferManager.ReturnBuffer(messageBuffer.Array);
                    messageBuffer = new ArraySegment<byte>();
                }
            }

            private void CompleteSend(IAsyncResult result, bool synchronous)
            {
                try
                {
                    int bytesSent = socket.EndSendTo(result);

                    if (bytesSent != messageBuffer.Count)
                    {
                        throw new CommunicationException(string.Format(CultureInfo.CurrentCulture,
                                                                       "A Udp error occurred sending a message to {0}.",
                                                                       remoteEndpoint));
                    }
                }
                catch (SocketException socketException)
                {
                    throw UdpChannelHelpers.ConvertTransferException(socketException);
                }
                finally
                {
                    CleanupBuffer();
                }

                base.Complete(synchronous);
            }

            private void OnSend(IAsyncResult result)
            {
                if (result.CompletedSynchronously)
                    return;

                try
                {
                    CompleteSend(result, false);
                }
                catch (Exception e)
                {
                    base.Complete(false, e);
                }
            }

            public static void End(IAsyncResult result)
            {
                End<SendAsyncResult>(result);
            }
        }

        #endregion

        private ArraySegment<byte> EncodeMessage(Message message)
        {
            try
            {
                return encoder.WriteMessage(message, int.MaxValue, null);
            }
            finally
            {
                // We have consumed the message by serializing it, so clean up
                message.Close();
            }
        }

    }
}